2.1 哈希函数:碰撞阻力、隐藏与谜题友好性
为什么需要哈希函数?
如果说区块链是一栋大楼,哈希函数(Hash Function)就是大楼的钢筋混凝土——它无处不在:交易ID由哈希生成,区块头通过哈希链式链接,地址由公钥哈希派生,Merkle 树依赖哈希归约。理解哈希函数,是理解一切上层密码学机制的前提。
定义:哈希函数 将任意长度的二进制输入映射为固定长度(如 位)的输出,且满足以下四个基本性质:
| 性质 | 含义 | 直觉类比 |
|---|---|---|
| 确定性 | 同一输入始终产生相同的输出 | 同一指紋对应同一人 |
| 快速计算 | 给定输入可在极短时间内计算出结果 | 扫描仪瞬间生成电子指纹 |
| 单向性(原像抗性) | 已知 无法反推 | 从指纹无法还原出人体 |
| 抗碰撞性 | 无法找到 使 | 不可能两指指纹相同 |
碰撞阻力(Collision Resistance)— 安全根基
碰撞阻力的形式化定义:存在一个可忽略函数 ,使得任何多项式时间的攻击者找到碰撞的概率满足:
为什么需要抗碰撞?以数字签名为例——签名者签名的是 而非 本身。如果攻击者能找到两个不同的消息 使得 ,他就可以让签名者为 签署的签名同时被解释为对 的有效签名。
生日攻击与安全强度:直觉上你可能认为找到碰撞需要尝试 次,但生日悖论(Birthday Paradox)告诉我们只需约 次。因此 SHA-256 的抗碰撞安全强度仅为 128-bit(而非 256-bit)。
小实验:全班 23 人中就有约 50% 的概率至少两人同一天生日——这就是生日悖论。碰撞搜索的下界同样由这一原理决定。
隐藏性(Hiding)— 承诺机制的基础
隐藏性比单向性更强:给定 ,不仅无法反推出 ,而且无法获取 的任何信息。
承诺机制(Commitment Scheme)示例:
- 承诺阶段:Alice 写下秘密数字 ,计算 并将 发送给 Bob。
- 揭晓阶段:Alice 将 发送给 Bob,Bob 验证 。
安全性保证:
- Bob 在揭晓前无法知道 (隐藏性由 保证)
- Alice 无法事后更换为 (绑定性由碰撞阻力保证)
实际应用:密封投标、链上随机数生成、Layer 2 状态承诺。
谜题友好性(Puzzle-Friendliness)— PoW 的理论基础
谜题友好性要求:不存在比穷举搜索更高效的策略来找到满足特定输出条件的输入。形式化描述:给定目标集合 ,需要尝试约 次才能找到 使 。
比特币 PoW 中的体现:矿工不断调整区块头中的 nonce,寻找:
每个 nonce 产生一个伪随机输出,全无捷径可走——这正是"工作量"(work)的数学来源。
长度扩展攻击与算法选型
Merkle-Damgård(MD)结构有一个固有缺陷:给定 ,可以在不知道 的情况下计算 。这称为长度扩展攻击(Length Extension Attack)。
- 受影响:MD5、SHA-1、SHA-256(均基于 MD 结构)
- 不受影响:SHA-3(Keccak,海绵结构)
- 教训:使用 HMAC 构造代替简单的 来抵御此类攻击
以太坊选择 Keccak-256 而非 SHA-256 的部分原因,正是 Keccak 天然免疫长度扩展攻击。
本节要点
- 哈希函数是单向、确定性、抗碰撞的"数字指纹"函数
- 碰撞阻力由生日攻击决定安全强度( bit,SHA-256 提供 128-bit)
- 隐藏性为承诺机制提供保障,谜题友好性为 PoW 挖矿奠定理论基础
- 长度扩展攻击是 MD 结构的安全缺陷,影响算法选型
2.2 SHA-256 与 Keccak-256 算法演练
SHA-256 和 Keccak-256 是区块链世界最核心的两种哈希算法:比特币采用 SHA-256,以太坊采用 Keccak-256。两者虽然都输出 256-bit 摘要,但内部结构截然不同。
SHA-256:Merkle-Damgård 结构与压缩函数
SHA-256 基于 Merkle-Damgård 迭代构造,整体流程如下:
flowchart LR
M["消息 M"] --> P["填充 & 分块"]
P --> M1["M₁ (512-bit)"]
P --> M2["M₂ (512-bit)"]
P --> Mn["Mₙ (512-bit)"]
M1 --> C1["压缩函数 f"]
IV["IV (H₀)"] --> C1
C1 --> H1["H₁"]
H1 --> C2["压缩函数 f"]
M2 --> C2
C2 --> H2["H₂"]
H2 --> Cn["..."]
Mn --> Cn
Cn --> Hn["Hₙ → 256-bit 摘要"]
处理步骤:
- 消息填充:附加一个 '1' 比特、k 个 '0' 比特、64-bit 长度域,使总长度是 512-bit 的整数倍。
- 消息分块:将填充后的消息分为 ,每块 512-bit(64 字节)。
- 压缩函数迭代:,其中 为 8 个 32-bit 初始寄存器值(IV)。
- 输出: 的 8 个寄存器拼接为 256-bit 摘要。
单轮压缩函数内部包含 64 轮迭代。以下是 1 轮的更新逻辑:
flowchart TD
subgraph 输入
A0["A₀"]; B0["B₀"]; C0["C₀"]; D0["D₀"]
E0["E₀"]; F0["F₀"]; G0["G₀"]; H0["H₀"]
Wt["Wₜ (轮消息字)"]; Kt["Kₜ (轮常数)"]
end
subgraph 一轮运算
Ch["Ch(E,F,G)"] --> Sum1["Σ₁(E)"]
Sum1 --> T1["T₁ = H + Σ₁ + Ch + Wₜ + Kₜ"]
Maj["Maj(A,B,C)"] --> Sum0["Σ₀(A)"]
Sum0 --> T2["T₂ = Σ₀ + Maj"]
end
T1 --> newE["E₁ ← D₀ + T₁"]
T2 --> newA["A₁ ← T₁ + T₂"]
B0 --> newB["B₁ ← A₀"]
C0 --> newC["C₁ ← B₀"]
D0 --> newD["D₁ ← C₀"]
F0 --> newF["F₁ ← E₀"]
G0 --> newG["G₁ ← F₀"]
H0 --> newH["H₁ ← G₀"]
核心逻辑函数(每轮使用):
Keccak-256:海绵结构(Sponge Construction)
Keccak-256 采用与 SHA-256 根本不同的海绵结构。它不是迭代压缩,而是通过"吸收(Absorbing)→ 挤压(Squeezing)"两个阶段工作:
flowchart LR
subgraph 吸收阶段
M1["M₁"] --> X1["⊕"] --> F1["f 置换"]
M2["M₂"] --> X2["⊕"] --> F2["f 置换"]
M3["M₃"] --> X3["⊕"] --> F3["f 置换"]
F1 --> X2
F2 --> X3
end
subgraph 挤压阶段
F3 --> O1["输出块 1"]
O1 --> F4["f 置换"]
F4 --> O2["输出块 2"]
O2 --> F5["..."]
F5 --> O3["截断至 256-bit"]
end
状态与参数:内部状态是一个 bits = 1600 bits 的三维数组。参数配置为 capacity bits、bitrate bits,满足 。
与 SHA-256 的关键差异对比:
| 维度 | SHA-256 | Keccak-256 |
|---|---|---|
| 构造方式 | Merkle-Damgård(迭代压缩) | 海绵结构(吸收+挤压) |
| 内部运算 | 64 轮压缩函数(Ch/Maj/Σ) | Keccak-f[1600] 置换(24 轮:θ,ρ,π,χ,ι) |
| 抗长度扩展 | 否(MD 固有缺陷) | 是(海绵结构天然免疫) |
| 输出截断 | 直接取 Hₙ | 从状态中挤压并截断 |
| 使用链 | 比特币、BCH、BSV | 以太坊(原始 Keccak-256) |
Python 代码示例
示例 1:对同一输入计算 SHA-256 与 Keccak-256
import hashlib
# 方法一:使用 PyCryptodome 的 Keccak-256(与以太坊一致)
from Crypto.Hash import keccak
message = b"Hello, Crypto World!"
# SHA-256
sha256_hash = hashlib.sha256(message).hexdigest()
print(f"SHA-256: {sha256_hash}")
# Keccak-256(以太坊版本)
keccak_hash_obj = keccak.new(digest_bits=256)
keccak_hash_obj.update(message)
keccak256_hash = keccak_hash_obj.hexdigest()
print(f"Keccak-256: {keccak256_hash}")
# 验证长度
print(f"SHA-256 长度: {len(sha256_hash)} 字符 (256 bits)")
print(f"Keccak-256 长度: {len(keccak256_hash)} 字符 (256 bits)")预期输出(近似):
SHA-256: 7b83b4b26a2a5d17e80a9c4c8b1cf0b8e7c0e8a9f5d3c2b1a0f9e8d7c6b5a4b3
Keccak-256: 9c2e5d8a1f3b6c4e7d0a9b8c7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f两个输出长度相同但内容完全不同,体现了不同算法架构导致的不可预测性。
示例 2:雪崩效应演示
import hashlib
from Crypto.Hash import keccak
def hamming_distance(hex1, hex2):
"""计算两个 hex 字符串的 bit 级汉明距离"""
bits1 = bin(int(hex1, 16))[2:].zfill(256)
bits2 = bin(int(hex2, 16))[2:].zfill(256)
return sum(b1 != b2 for b1, b2 in zip(bits1, bits2))
original = b"hello"
modified = b"hellp" # 仅修改 1 个字符('o' → 'p')
# SHA-256 雪崩
sha_orig = hashlib.sha256(original).hexdigest()
sha_mod = hashlib.sha256(modified).hexdigest()
sha_hd = hamming_distance(sha_orig, sha_mod)
# Keccak-256 雪崩
k = keccak.new(digest_bits=256)
k.update(original)
kec_orig = k.hexdigest()
k = keccak.new(digest_bits=256)
k.update(modified)
kec_mod = k.hexdigest()
kec_hd = hamming_distance(kec_orig, kec_mod)
print(f"原始输入: {original}")
print(f"修改输入: {modified}")
print()
print(f"SHA-256 原始: {sha_orig}")
print(f"SHA-256 修改: {sha_mod}")
print(f"SHA-256 汉明距离: {sha_hd} bits ({sha_hd/256*100:.1f}%)")
print()
print(f"Keccak-256 原始: {kec_orig}")
print(f"Keccak-256 修改: {kec_mod}")
print(f"Keccak-256 汉明距离: {kec_hd} bits ({kec_hd/256*100:.1f}%)")预期输出分析:两种算法的汉明距离均应接近 128 bits(),满足严格雪崩准则(SAC, Strict Avalanche Criterion)。修改输入中的 1 个比特,会导致输出中约一半的比特位翻转,且翻转位置分布均匀。这一特性使攻击者无法通过部分输入-输出对应关系推断整体映射。
本节要点
- SHA-256 基于 Merkle-Damgård 结构,通过 64 轮压缩函数迭代产生 256-bit 摘要,易受长度扩展攻击
- Keccak-256 基于海绵结构,通过吸收-挤压两阶段产生输出,天然免疫长度扩展攻击
- 两种算法均满足严格雪崩准则(SAC),输入 1 bit 变化导致约 128 bits 输出翻转
- 以太坊选择 Keccak-256 综合了安全性与生态系统兼容性的考量
2.3 对称加密与非对称加密
对称加密:快速但需共享密钥
对称加密(Symmetric Encryption) 使用同一个密钥进行加密和解密。发送方用密钥 加密明文 得到密文 ,接收方用同一密钥 解密密文 恢复明文 。
在区块链生态中,对称加密主要用于:
- 本地钱包文件加密:比特币核心客户端(Bitcoin Core)使用 AES-256-CBC 加密钱包文件,用户只需记住一个钱包密码
- 节点间 TLS 通信:比特币/以太坊节点之间的 P2P 通信使用 TLS(传输层安全协议)加密传输,防范中间人攻击
代表性对称加密算法:AES(Advanced Encryption Standard,高级加密标准)、ChaCha20。
核心特点:加密速度快(硬件级支持)、密钥短(AES-256 仅 32 字节);但密钥分发困难——双方必须通过安全渠道预先共享密钥。
非对称加密:区块链中的常见误解
非对称加密(Asymmetric Encryption) 使用一对密钥:公钥(Public Key) 公开分发,私钥(Private Key) 秘密持有。
很多人认为区块链大量使用了非对称加密,这是一个需要澄清的重要误解。事实上,区块链协议中几乎不使用非对称加密来加密数据——因为区块链上的数据需要公开验证,加密与此目标矛盾。区块链中真正大量使用的是数字签名(Digital Signature),它是非对称密码学的一个变体,但目标不是"隐藏信息"而是"证明身份与授权"。
flowchart LR
subgraph 对称加密
direction LR
A1["明文 M"] --> E1["加密 Eₖ(M)"]
K1["密钥 K"] --> E1
E1 --> C1["密文 C"]
C1 --> D1["解密 Dₖ(C)"]
K1 --> D1
D1 --> P1["明文 M"]
end
subgraph asym["数字签名(非对称变体)"]
direction LR
A2["消息 M"] --> H2["哈希 H(M)"]
H2 --> S2["签名 Signₛₖ(H)"]
SK["私钥 sk"] --> S2
S2 --> SIG["签名 σ"]
SIG --> V2["验证 Verifyₚₖ(H,σ)"]
PK["公钥 pk"] --> V2
V2 --> R2["✅ 有效 / ❌ 无效"]
end
类比理解:对称加密像一把钥匙开一把锁(双方持有同一把钥匙);数字签名像一枚私章(印章在手中,任何人都可以用你的公开印鉴比对确认)。
| 维度 | 对称加密 | 数字签名 |
|---|---|---|
| 密钥数量 | 1 个(共享密钥) | 2 个(私钥+公钥) |
| 主要用途 | 数据保密 | 身份认证+完整性 |
| 区块链角色 | 钱包加密、TLS通信 | 交易签名、地址派生 |
| 典型算法 | AES, ChaCha20 | ECDSA, Schnorr, EdDSA |
| 性能 | 极快(GB/s级) | 较慢(数千次/秒) |
本节要点
- 对称加密在区块链中用于钱包文件加密和 P2P 通信加密
- 非对称加密在区块链中的主要应用是数字签名,而非数据加密
- 公链数据需要公开验证,因此加密不是常态
2.4 椭圆曲线密码学(ECC)与 secp256k1
为什么区块链选择 ECC 而非 RSA?
比特币和以太坊均选择椭圆曲线密码学(ECC, Elliptic Curve Cryptography)作为签名算法的基础,原因在于效率:ECC 在更短的密钥长度下提供同等安全强度。
| 安全强度(bit) | RSA 密钥长度 | ECC 密钥长度 |
|---|---|---|
| 80 | 1024-bit | 160-bit |
| 128 | 3072-bit | 256-bit |
| 256 | 15360-bit | 512-bit |
上表可以看出:要达到 128-bit 安全强度,RSA 需要 3072-bit 密钥(384 字节),而 ECC 只需 256-bit 密钥(32 字节)。对于区块链而言,更短的密钥意味着更小的交易体积和更快的验签速度。
secp256k1 曲线参数
比特币和以太坊均采用 secp256k1 曲线,由 Certicom 标准定义。其曲线方程为:
完整参数:
| 参数 | 值 | 说明 |
|---|---|---|
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F | 有限域的素数(256-bit) | |
0x0000000000000000000000000000000000000000000000000000000000000000 | 曲线系数 | |
0x0000000000000000000000000000000000000000000000000000000000000007 | 曲线系数 | |
(0x79BE667E..., 0x483ADA77...) | 生成点(Generator) | |
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 | 基点阶(素数) | |
0x01 | 余因子(Cofactor) |
椭圆曲线点运算的几何直觉
椭圆曲线上的点加法(Point Addition)有直观的几何解释:给定曲线上两点 和 ,作直线穿过 和 交曲线于第三点 , 关于 x 轴的对称点即为 。
倍点运算(Point Doubling) 是点加法的特殊形式:当 时,取 点的切线,交曲线于点 ,对称后得到 。
公钥推导与离散对数难题
在 secp256k1 上,私钥 是一个随机选择的 256-bit 整数,公钥 通过标量乘法(Scalar Multiplication)计算:
即私钥 乘以生成点 。这个运算本身很容易,但反过来——给定公钥 和生成点 求私钥 ——被认为是计算上不可行的,这称为椭圆曲线离散对数难题(ECDLP, Elliptic Curve Discrete Logarithm Problem)。
从 secp256k1 的基点阶 可知,ECDLP 的破解复杂度约为 次曲线运算,这也是 256-bit ECC 提供 128-bit 安全强度的原因。
flowchart LR
SK["私钥 d<br/>(256-bit 随机数)"] --> M["标量乘法<br/>P = d × G"]
G["生成点 G<br/>(secp256k1 基点)"] --> M
M --> PK["公钥 P<br/>(x,y 坐标, 512-bit)"]
PK --> H["哈希 H(P)"]
H --> ADDR["地址<br/>(160-bit / 256-bit)"]
本节要点
- ECC 在相同安全强度下比 RSA 密钥短 10 倍以上,是区块链密码学的基础
- secp256k1 是比特币和以太坊共同采用的椭圆曲线标准
- 公钥推导 正向容易、逆向极难(ECDLP)
2.5 数字签名与验签:ECDSA 详解
签名与验签流程
ECDSA(Elliptic Curve Digital Signature Algorithm,椭圆曲线数字签名算法)是比特币和以太坊(legacy 地址)使用的签名方案。
签名(对消息 ,私钥 ):
- 计算消息哈希 (比特币的双重哈希)
- 选取随机数
- 计算椭圆曲线点
- 计算 ;若 则重新选择
- 计算 ;若 则重新选择
- 输出签名
验签(对消息 ,签名 ,公钥 ):
- 验证
- 计算
- 计算
- 计算 ,
- 计算点
- 当且仅当 时签名有效
flowchart TD
subgraph sign["签名(Sign)"]
M1["消息 m"] --> H1["双 SHA-256"]
H1 --> E1["e = SHA256²(m)"]
SK["私钥 d"] --> S1
K["随机数 k"] --> S1
E1 --> S1["s = k⁻¹(e + r·d)"]
G["基点 G"] --> P1["(x₁,y₁) = k×G"]
P1 --> R1["r = x₁ mod n"]
R1 --> SIG["签名 (r, s)"]
S1 --> SIG
end
subgraph verify["验签(Verify)"]
SIG --> V1["验证 r,s ∈ [1,n-1]"]
M2["消息 m"] --> H2["双 SHA-256"]
H2 --> E2["e = SHA256²(m)"]
PK["公钥 P"] --> V2
G --> V2
E2 --> V2["(x₁,y₁) = e·w×G + r·w×P"]
V1 --> V2
V2 --> R2["✅ r ≡ x₁ (mod n)?"]
end
为什么 必须随机且唯一
是 ECDSA 签名中最关键的安全参数。如果 被重复使用(即对两个不同的消息使用相同的 ),攻击者可以直接计算出私钥:
真实教训:Sony PS3 私钥泄露事件(2010 年)。Sony 在 PlayStation 3 的 ECDSA 实现中使用了固定的 (硬件随机数生成器未正确实现),导致黑客在 2010 年成功提取了 Sony 的 ECDSA 私钥,使得数以百万计的 PS3 被自由破解。这个案例被广泛用作"永远不要重用 "的经典警示。
Python 代码示例:ECDSA 签名与验签
from ecdsa import SECP256k1, SigningKey, VerifyingKey
import hashlib
# 1. 生成私钥(自动随机生成 k 的安全实现)
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.verifying_key
# 待签名消息
message = b"Transfer 1 BTC to Alice"
# 2. 签 名(使用 SHA-256 哈希)
signature = sk.sign(message, hashfunc=hashlib.sha256)
print(f"签名长度: {len(signature)} 字节")
print(f"签名 (hex): {signature.hex()}")
# 3. 验 签(正确消息)
try:
vk.verify(signature, message, hashfunc=hashlib.sha256)
print("✅ 验签通过:消息完整且签名有效")
except:
print("❌ 验签失败")
# 4. 篡改消息后验签
tampered_message = b"Transfer 1 BTC to Bob"
try:
vk.verify(signature, tampered_message, hashfunc=hashlib.sha256)
print("❌ 验签通过(不应发生!)")
except:
print("✅ 篡改检测:验签按预期失败")预期输出:
签名长度: 70-72 字节(DER 编码可变长)
签名 (hex): 30440220...
✅ 验签通过:消息完整且签名有效
✅ 篡改检测:验签按预期失败从 ECDSA 到 Schnorr:下一步的展望
ECDSA 虽然安全且经过长期验证,但存在一些局限性。比特币的 Taproot 升级(2021 年) 引入了 Schnorr 签名,相比 ECDSA 有以下优势:
| 维度 | ECDSA | Schnorr |
|---|---|---|
| 签名大小 | 70-72 字节(DER 编码) | 64 字节(固定) |
| 线性聚合 | ❌ 不支持 | ✅ 支持(MuSig 等) |
| 批验证 | 需逐一验签 | 一批签名可一次性验证 |
| 线性性 | 非线性(复杂) | 线性(数学优雅) |
| 比特币适配 | Legacy / SegWit 地址 | Taproot (P2TR) |
Schnorr 的线性聚合特性使得多签交易(如 2/3 多签钱包)的链上成本从多个签名合并为一个,大大降低了费用并提升了隐私性。这为后续的 BLS 签名(Boneh-Lynn-Shacham)和更复杂的门限签名铺平了道路。
本节要点
- ECDSA 签名包含 两个值,验签需要公钥、消息和签名
- 随机数 必须每次唯一且随机,否则私钥将泄露(Sony PS3 教训)
- Schnorr 签名是 ECDSA 的下一代替代方案,支持签名聚合
2.6 钱包、私钥与助记词的安全原理
从熵到助记词:BIP-39
用户与区块链交互的入口是钱包(Wallet)。钱包不"存钱"——它存储的是私钥(Private Key),也就是控制链上资产的唯一凭证。为了让私钥易于备份和恢复,业界采用了 BIP-39 标准,将随机熵编码为一串自然语言单词。
生成流程:
- 熵(Entropy):生成 128-bit(12 词)或 256-bit(24 词)的随机数
- 校验和(Checksum):取熵的 SHA-256 前若干位(熵长/32),附加在熵末尾
- 分割与映射:将(熵 + 校验和)按 11-bit 一组分割,每组对应 BIP-39 词表中的 2048 个单词之一
12 词 vs 24 词:
| 维度 | 12 词 | 24 词 |
|---|---|---|
| 熵长度 | 128-bit | 256-bit |
| 安全强度 | 128-bit | 256-bit |
| 助记词长度 | 12 单词 | 24 单词 |
| 适合场景 | 高频使用(冷热钱包) | 长期自托管(冷存储) |
128-bit 的安全强度对个人用户已绰绰有余,因此 12 词是行业主流。24 词在面临量子计算威胁(Grover 算法将 256-bit 降为 128-bit)时仍有余量。
从助记词到种子:PBKDF2
助记词不能直接用作私钥——需要经过 PBKDF2(Password-Based Key Derivation Function 2)密钥派生函数:
其中:
- 盐值(Salt):固定字符串
"mnemonic"拼接可选口令(passphrase) - 迭代次数:2048 次 HMAC-SHA512 迭代(BIP-39 标准)
- 可选口令(第 25 词)提供额外的保护层:使用不同口令从同一组助记词可派生完全不同的种子集
HD 钱包与 BIP-32/BIP-44
层级确定性钱包(HD Wallet, Hierarchical Deterministic Wallet) 的核心思想:从一个种子(Seed)通过一个主密钥和层级路径派生无限个子私钥/公钥。
BIP-32 定义了从主密钥派生子密钥的数学机制(CKDF,Child Key Derivation Function)。BIP-44 在此基础上定义了资产发现路径的行业标准:
m / purpose' / coin_type' / account' / change / address_index以以太坊地址路径为例:m/44'/60'/0'/0/0
| 层级 | 值 | 含义 |
|---|---|---|
| purpose | 44' | BIP-44 标准 |
| coin_type | 60' | 以太坊(比特币为 0') |
| account | 0' | 账户索引 |
| change | 0 | 外部链(接收地址) |
| address_index | 0 | 地址序号 |
(上标撇号 ' 表示硬化派生(Hardened Derivation),防止子私钥泄露导致父公钥被反向推导。)
flowchart LR
E["熵 (128-bit)"] --> S["SHA-256 校验和<br/>前 4-bit"]
E --> C["拼接"]
S --> C
C --> G["11-bit 分组<br/>(×12)"]
G --> M["BIP-39 词表<br/>→ 12 个助记词"]
M --> K["PBKDF2"]
K --> SEED["种子 (512-bit)"]
SEED --> MK["主密钥"]
MK --> P1["BIP-44<br/>m/44'/60'/0'/0/0"]
MK --> P2["BIP-44<br/>m/44'/60'/0'/0/1"]
P1 --> PK1["私钥 → 公钥 → 地址"]
P2 --> PK2["私钥 → 公钥 → 地址"]
安全实践
- 冷存储:离线生成私钥并用物理介质(纸质/金属助记词板)保存,永不连接互联网
- 硬件钱包:专用芯片在隔离环境中签名交易,私钥永不离开设备
- 助记词物理隔离:建议将助记词分多份异地存储,避免单点灾难
- 钓鱼攻击防护:永远不在任何网站输入助记词——真正的钱包也永远不会要求你输入助记词
本节要点
- BIP-39 将 128-bit 熵编码为 12 个单词,便于人类记忆和备份
- HD 钱包通过 BIP-32/BIP-44 从单个种子派生无限个地址
- 助记词是控制链上资产的最终凭证,安全防护高于一切
2.7 默克尔树与简单支付验证(SPV)
默克尔树的构建
默克尔树(Merkle Tree) 是一种二叉树结构,叶子节点是数据块的哈希,非叶子节点是其子节点哈希拼接后的哈希。比特币使用 SHA-256 双重哈希构建默克尔树。
以 4 笔交易的默克尔树为例:
Root = H(H01 || H23)
/ \
H01 H23
/ \ / \
H0 H1 H2 H3
| | | |
Tx0 Tx1 Tx2 Tx3其中 ,,依此类推。
Merkle 路径证明原理
验证一笔交易是否包含在区块中,只需提供Merkle 路径(Merkle Path)——从该交易叶子节点到根节点沿途的兄弟哈希。
例如,要证明 Tx1 在树中,只需提供 (Tx1 的兄弟哈希)和 (H01 的兄弟哈希)。验证者计算:
- 比较 是否等于区块头中的默克尔根
核心优势:证明复杂度仅为 个哈希。对于包含 2000 笔交易的区块,只需约 11 个 32 字节的哈希即可完成验证——远小于下载全部交易。
SPV 客户端的工作机制
SPV(Simplified Payment Verification,简单支付验证)客户端不下载完整的区块数据,只下载区块头(每个 80 字节):
sequenceDiagram
participant SPV as SPV 客户端
participant FN as 全节点
Note over SPV: 下载所有区块头(~80 字节/个)
SPV->>FN: 请求区块头(从创世到最新)
FN-->>SPV: 返回区块头链
Note over SPV: 验证最长链(PoW 累积难度最大)
SPV->>FN: 我有一笔 tx: a1b2c3...
SPV->>FN: 请提供包含它的区块头索引和 Merkle 路径
FN-->>SPV: 区块高度 #800123 + [兄弟哈希列表]
Note over SPV: 从 Merkle 路径计算根哈希
Note over SPV: 与已知区块头中的 Merkle 根比对
alt 一致 ✅
SPV->>SPV: 确认交易已被包含
else 不一致 ❌
SPV->>SPV: 拒绝该交易
end
Python 代码:构建并验证默克尔树
import hashlib
def sha256d(data: bytes) -> bytes:
"""双重 SHA-256"""
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
class MerkleTree:
def __init__(self, tx_hashes: list[bytes]):
self.leaves = tx_hashes
self.tree = self._build_tree(tx_hashes)
self.root = self.tree[-1][0] if self.tree else None
def _build_tree(self, nodes: list[bytes]) -> list[list[bytes]]:
tree = [nodes]
while len(nodes) > 1:
if len(nodes) % 2 == 1:
nodes = nodes + [nodes[-1]] # 奇数时复制最后一个
next_level = []
for i in range(0, len(nodes), 2):
combined = nodes[i] + nodes[i + 1]
next_level.append(sha256d(combined))
tree.append(next_level)
nodes = next_level
return tree
def get_proof(self, index: int) -> list[bytes]:
"""获取叶子节点 index 的 Merkle 路径"""
proof = []
for level in self.tree[:-1]: # 不包括根
sibling_index = index ^ 1 # 异或:0→1, 1→0, 2→3...
if sibling_index < len(level):
proof.append(level[sibling_index])
index //= 2
return proof
def verify_proof(tx_hash: bytes, proof: list[bytes], root: bytes) -> bool:
"""验证 Merkle 路径"""
current = tx_hash
for sibling in proof:
# 确定拼接顺序
if current < sibling: # 按字典序
combined = current + sibling
else:
combined = sibling + current
current = sha256d(combined)
return current == root
# --- 演示 ---
txs = [f"tx_{i}".encode() for i in range(6)]
tx_hashes = [sha256d(tx) for tx in txs]
tree = MerkleTree(tx_hashes)
print(f"默克尔根: {tree.root.hex()}")
# 证明 tx_2(index=2)的包含性
proof = tree.get_proof(2)
tx_hash = tx_hashes[2]
valid = verify_proof(tx_hash, proof, tree.root)
print(f"✅ Tx_2 包含性验证: {valid}")
# 证明伪造交易
fake_tx_hash = sha256d(b"fake_tx")
valid = verify_proof(fake_tx_hash, proof, tree.root)
print(f"❌ 伪造交易验证: {valid}")预期输出:
默克尔根: 4f3c8c...
✅ Tx_2 包含性验证: True
❌ 伪造交易验证: FalseSPV 的安全局限
SPV 客户端只能验证"交易是否被包含在某区块中",无法验证:
- 区块是否包含无效交易(如双花)
- 共识规则是否被遵守(如是否超发)
- 区块头是否来自合法链分叉
因此 SPV 客户端的信任模型是:假定全节点诚实且最长链上的区块头都是合法的。对于需要最高安全性的场景(如大额交易),运行全节点更可靠。
本节要点
- 默克尔树通过 个哈希实现海量交易的包含性验证
- SPV 客户端仅下载 80 字节/块的区块头和 Merkle 路径,无需全链数据
- SPV 不能验证规则合规性,适用于轻量级但信任全节点的场景
2.8 本章小结
第 2 章是整部教程中密码学密度最高的一章。我们从最底层的哈希函数出发,经历了 SHA-256 的 64 轮压缩、Keccak-256 的海绵吸水、椭圆曲线的点乘运算、ECDSA 的签名与验签,最终落地到用户最熟悉也最关心的钱包和助记词。
flowchart LR
subgraph 密码学基石
H["哈希函数<br/>SHA-256 / Keccak-256"]
ECC["椭圆曲线<br/>secp256k1"]
SIG["数字签名<br/>ECDSA / Schnorr"]
MR["默克尔树<br/>Merkle Tree"]
end
H --> ECC
H --> MR
ECC --> SIG
SIG --> W["钱包/地址"]
MR --> SPV["轻客户端验证"]
带走以下三个关键认知:
- 哈希链是实现不可篡改的"胶水"。区块之间通过哈希指针链接,Merkle 根浓缩了区块内的所有交易,哈希的连锁反应使任何历史篡改都会在链式结构中暴露无遗。
- 椭圆曲线使私钥短而安全。32 字节的 secp256k1 私钥提供的 128-bit 安全强度,在传统 RSA 体系下需要 3072-bit 密钥。这个"短"是区块链交易得以小额高效的关键工程基础。
- 默克尔树让轻验证成为可能。没有默克尔树,每个轻客户端都需要下载全部交易来验证包含性;有了它,只需 个哈希就能完成验证——这是移动钱包和浏览器插件钱包能够运行的数学根基。
评论
0评论加载中…